using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Objects;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;

namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World;

/// <summary>A command which clears in-game objects.</summary>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ClearCommand : ConsoleCommand
{
    /*********
    ** Fields
    *********/
    /// <summary>The valid types that can be cleared.</summary>
    private readonly string[] ValidTypes = ["crops", "debris", "fruit-trees", "furniture", "grass", "trees", "removable", "everything"];

    /// <summary>The resource clump IDs to consider debris.</summary>
    private readonly int[] DebrisClumps = [ResourceClump.stumpIndex, ResourceClump.hollowLogIndex, ResourceClump.meteoriteIndex, ResourceClump.boulderIndex];


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    public ClearCommand()
        : base(
            name: "world_clear",
            description:
                """
                Clears in-game entities in a given location.

                Usage: world_clear <location> <object type>
                 - location: the location name for which to clear objects (like Farm), or 'current' for the current location.
                 - object type: the type of object clear. You can specify 'crops', 'debris' (stones/twigs/weeds and dead crops), 'furniture', 'grass', and 'trees' / 'fruit-trees'. You can also specify 'removable' (remove everything that can be removed or destroyed during normal gameplay) or 'everything' (remove everything including permanent bushes).
                """
        )
    { }

    /// <summary>Handle the command.</summary>
    /// <param name="monitor">Writes messages to the console and log file.</param>
    /// <param name="command">The command name.</param>
    /// <param name="args">The command arguments.</param>
    public override void Handle(IMonitor monitor, string command, ArgumentParser args)
    {
        // check context
        if (!Context.IsWorldReady)
        {
            monitor.Log("You need to load a save to use this command.", LogLevel.Error);
            return;
        }

        // parse arguments
        if (!args.TryGet(0, "location", out string? locationName, required: true))
            return;
        if (!args.TryGet(1, "object type", out string? type, required: true, oneOf: this.ValidTypes))
            return;

        // get target location
        GameLocation? location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase));
        if (location == null && locationName == "current")
            location = Game1.currentLocation;
        if (location == null)
        {
            string[] locationNames = (from loc in Game1.locations where !string.IsNullOrWhiteSpace(loc.Name) orderby loc.Name select loc.Name).ToArray();
            monitor.Log($"Could not find a location with that name. Must be one of [{string.Join(", ", locationNames)}].", LogLevel.Error);
            return;
        }

        // apply
        switch (type)
        {
            case "crops":
                {
                    int removed =
                        this.RemoveTerrainFeatures(location, p => p is HoeDirt)
                        + this.RemoveResourceClumps(location, p => p is GiantCrop);
                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            case "debris":
                {
                    int removed = 0;
                    foreach (var pair in location.terrainFeatures.Pairs.ToArray())
                    {
                        TerrainFeature feature = pair.Value;
                        if (feature is HoeDirt dirt && dirt.crop?.dead.Value is true)
                        {
                            dirt.crop = null;
                            removed++;
                        }
                    }

                    removed +=
                        this.RemoveObjects(location, obj =>
                            obj is not Chest
                            && (
                                obj.Name is "Weeds" or "Stone"
                                || obj.ParentSheetIndex is 294 or 295
                            )
                        )
                        + this.RemoveResourceClumps(location, clump => this.DebrisClumps.Contains(clump.parentSheetIndex.Value));

                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            case "fruit-trees":
                {
                    int removed = this.RemoveTerrainFeatures(location, feature => feature is FruitTree);
                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            case "furniture":
                {
                    int removed = this.RemoveFurniture(location, _ => true);
                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            case "grass":
                {
                    int removed = this.RemoveTerrainFeatures(location, feature => feature is Grass);
                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            case "trees":
                {
                    int removed = this.RemoveTerrainFeatures(location, feature => feature is Tree);
                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            case "removable":
            case "everything":
                {
                    bool everything = type == "everything";
                    int removed =
                        this.RemoveFurniture(location, _ => true)
                        + this.RemoveObjects(location, _ => true)
                        + this.RemoveTerrainFeatures(location, _ => true)
                        + this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable())
                        + this.RemoveResourceClumps(location, _ => true);
                    monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
                    break;
                }

            default:
                monitor.Log($"Unknown type '{type}'. Must be one [{string.Join(", ", this.ValidTypes)}].", LogLevel.Error);
                break;
        }
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Remove objects from a location matching a lambda.</summary>
    /// <param name="location">The location to search.</param>
    /// <param name="shouldRemove">Whether an entity should be removed.</param>
    /// <returns>Returns the number of removed entities.</returns>
    private int RemoveObjects(GameLocation location, Func<SObject, bool> shouldRemove)
    {
        int removed = 0;

        foreach ((Vector2 tile, SObject? obj) in location.Objects.Pairs.ToArray())
        {
            if (shouldRemove(obj))
            {
                location.Objects.Remove(tile);
                removed++;
            }
        }

        return removed;
    }

    /// <summary>Remove terrain features from a location matching a lambda.</summary>
    /// <param name="location">The location to search.</param>
    /// <param name="shouldRemove">Whether an entity should be removed.</param>
    /// <returns>Returns the number of removed entities.</returns>
    private int RemoveTerrainFeatures(GameLocation location, Func<TerrainFeature, bool> shouldRemove)
    {
        int removed = 0;

        foreach ((Vector2 tile, TerrainFeature? feature) in location.terrainFeatures.Pairs.ToArray())
        {
            if (shouldRemove(feature))
            {
                location.terrainFeatures.Remove(tile);
                removed++;
            }
        }

        return removed;
    }

    /// <summary>Remove large terrain features from a location matching a lambda.</summary>
    /// <param name="location">The location to search.</param>
    /// <param name="shouldRemove">Whether an entity should be removed.</param>
    /// <returns>Returns the number of removed entities.</returns>
    private int RemoveLargeTerrainFeatures(GameLocation location, Func<LargeTerrainFeature, bool> shouldRemove)
    {
        int removed = 0;

        foreach (LargeTerrainFeature feature in location.largeTerrainFeatures.ToArray())
        {
            if (shouldRemove(feature))
            {
                location.largeTerrainFeatures.Remove(feature);
                removed++;
            }
        }

        return removed;
    }

    /// <summary>Remove resource clumps from a location matching a lambda.</summary>
    /// <param name="location">The location to search.</param>
    /// <param name="shouldRemove">Whether an entity should be removed.</param>
    /// <returns>Returns the number of removed entities.</returns>
    private int RemoveResourceClumps(GameLocation location, Func<ResourceClump, bool> shouldRemove)
    {
        int removed = 0;

        foreach (ResourceClump clump in location.resourceClumps.Where(shouldRemove).ToArray())
        {
            location.resourceClumps.Remove(clump);
            removed++;
        }

        return removed;
    }

    /// <summary>Remove furniture from a location matching a lambda.</summary>
    /// <param name="location">The location to search.</param>
    /// <param name="shouldRemove">Whether an entity should be removed.</param>
    /// <returns>Returns the number of removed entities.</returns>
    private int RemoveFurniture(GameLocation location, Func<Furniture, bool> shouldRemove)
    {
        int removed = 0;

        foreach (Furniture furniture in location.furniture.ToArray())
        {
            if (shouldRemove(furniture))
            {
                location.furniture.Remove(furniture);
                removed++;
            }
        }

        return removed;
    }
}
